//! The common state for a module: any data useful over the full lifetime of its compilation that lives beyond individual IR's.
//!
//! Stores all interned data like idents, strings, and problems.
//!
//! This reduces the size of this module's IRs as they can store references to this
//! interned (and deduplicated) data instead of storing the values themselves.

const std = @import("std");
const builtin = @import("builtin");
const types_mod = @import("types");
const collections = @import("collections");
const base = @import("base");

const Node = @import("Node.zig");
const NodeStore = @import("NodeStore.zig");
const CIR = @import("CIR.zig");
const DependencyGraph = @import("DependencyGraph.zig");

const TypeWriter = types_mod.TypeWriter;
const CompactWriter = collections.CompactWriter;
const SortedArrayBuilder = collections.SortedArrayBuilder;
const CommonEnv = base.CommonEnv;
const Ident = base.Ident;
const StringLiteral = base.StringLiteral;
const RegionInfo = base.RegionInfo;
const Region = base.Region;
const SExprTree = base.SExprTree;
const SExpr = base.SExpr;
const TypeVar = types_mod.Var;
const TypeStore = types_mod.Store;

const Self = @This();

/// The kind of module being canonicalized, set during header processing
pub const ModuleKind = union(enum) {
    type_module: Ident.Idx, // Holds the main type identifier for type modules
    default_app,
    app,
    package,
    platform,
    hosted,
    deprecated_module,
    malformed,

    /// Extern-compatible tag for serialization
    pub const Tag = enum(u32) {
        type_module,
        default_app,
        app,
        package,
        platform,
        hosted,
        deprecated_module,
        malformed,
    };

    /// Extern-compatible payload union for serialization
    pub const Payload = extern union {
        type_module_ident: Ident.Idx,
        none: u32,
    };

    /// Extern-compatible serialized form
    pub const Serialized = extern struct {
        tag: Tag,
        payload: Payload,

        pub fn encode(kind: ModuleKind) @This() {
            return switch (kind) {
                .type_module => |idx| .{ .tag = .type_module, .payload = .{ .type_module_ident = idx } },
                .default_app => .{ .tag = .default_app, .payload = .{ .none = 0 } },
                .app => .{ .tag = .app, .payload = .{ .none = 0 } },
                .package => .{ .tag = .package, .payload = .{ .none = 0 } },
                .platform => .{ .tag = .platform, .payload = .{ .none = 0 } },
                .hosted => .{ .tag = .hosted, .payload = .{ .none = 0 } },
                .deprecated_module => .{ .tag = .deprecated_module, .payload = .{ .none = 0 } },
                .malformed => .{ .tag = .malformed, .payload = .{ .none = 0 } },
            };
        }

        pub fn decode(self: @This()) ModuleKind {
            return switch (self.tag) {
                .type_module => .{ .type_module = self.payload.type_module_ident },
                .default_app => .default_app,
                .app => .app,
                .package => .package,
                .platform => .platform,
                .hosted => .hosted,
                .deprecated_module => .deprecated_module,
                .malformed => .malformed,
            };
        }
    };
};

/// Well-known identifiers that are interned once and reused throughout compilation.
/// These are needed for type checking, operator desugaring, and layout generation.
/// This is an extern struct so it can be embedded in serialized ModuleEnv.
pub const CommonIdents = extern struct {
    // Method names for operator desugaring
    from_int_digits: Ident.Idx,
    from_dec_digits: Ident.Idx,
    plus: Ident.Idx,
    minus: Ident.Idx,
    times: Ident.Idx,
    div_by: Ident.Idx,
    div_trunc_by: Ident.Idx,
    rem_by: Ident.Idx,
    negate: Ident.Idx,
    abs: Ident.Idx,
    abs_diff: Ident.Idx,
    not: Ident.Idx,
    is_lt: Ident.Idx,
    is_lte: Ident.Idx,
    is_gt: Ident.Idx,
    is_gte: Ident.Idx,
    is_eq: Ident.Idx,

    // Type/module names
    @"try": Ident.Idx,
    out_of_range: Ident.Idx,
    builtin_module: Ident.Idx,
    str: Ident.Idx,
    list: Ident.Idx,
    box: Ident.Idx,

    // Fully-qualified type identifiers for type checking and layout generation
    builtin_try: Ident.Idx,
    builtin_numeral: Ident.Idx,
    builtin_str: Ident.Idx,
    u8_type: Ident.Idx,
    i8_type: Ident.Idx,
    u16_type: Ident.Idx,
    i16_type: Ident.Idx,
    u32_type: Ident.Idx,
    i32_type: Ident.Idx,
    u64_type: Ident.Idx,
    i64_type: Ident.Idx,
    u128_type: Ident.Idx,
    i128_type: Ident.Idx,
    f32_type: Ident.Idx,
    f64_type: Ident.Idx,
    dec_type: Ident.Idx,

    // Field/tag names used during type checking and evaluation
    before_dot: Ident.Idx,
    after_dot: Ident.Idx,
    provided_by_compiler: Ident.Idx,
    tag: Ident.Idx,
    payload: Ident.Idx,
    is_negative: Ident.Idx,
    digits_before_pt: Ident.Idx,
    digits_after_pt: Ident.Idx,
    box_method: Ident.Idx,
    unbox_method: Ident.Idx,
    to_inspect: Ident.Idx,
    ok: Ident.Idx,
    err: Ident.Idx,
    from_numeral: Ident.Idx,
    true_tag: Ident.Idx,
    false_tag: Ident.Idx,
    // from_utf8 result fields
    byte_index: Ident.Idx,
    string: Ident.Idx,
    is_ok: Ident.Idx,
    problem_code: Ident.Idx,
    // from_utf8 error payload fields (BadUtf8 record)
    problem: Ident.Idx,
    index: Ident.Idx,
    // Synthetic identifiers for ? operator desugaring
    question_ok: Ident.Idx,
    question_err: Ident.Idx,

    /// Insert all well-known identifiers into a CommonEnv.
    /// Use this when creating a fresh ModuleEnv from scratch.
    pub fn insert(gpa: std.mem.Allocator, common: *CommonEnv) std.mem.Allocator.Error!CommonIdents {
        return .{
            .from_int_digits = try common.insertIdent(gpa, Ident.for_text(Ident.FROM_INT_DIGITS_METHOD_NAME)),
            .from_dec_digits = try common.insertIdent(gpa, Ident.for_text(Ident.FROM_DEC_DIGITS_METHOD_NAME)),
            .plus = try common.insertIdent(gpa, Ident.for_text(Ident.PLUS_METHOD_NAME)),
            .minus = try common.insertIdent(gpa, Ident.for_text("minus")),
            .times = try common.insertIdent(gpa, Ident.for_text("times")),
            .div_by = try common.insertIdent(gpa, Ident.for_text("div_by")),
            .div_trunc_by = try common.insertIdent(gpa, Ident.for_text("div_trunc_by")),
            .rem_by = try common.insertIdent(gpa, Ident.for_text("rem_by")),
            .negate = try common.insertIdent(gpa, Ident.for_text(Ident.NEGATE_METHOD_NAME)),
            .abs = try common.insertIdent(gpa, Ident.for_text("abs")),
            .abs_diff = try common.insertIdent(gpa, Ident.for_text("abs_diff")),
            .not = try common.insertIdent(gpa, Ident.for_text("not")),
            .is_lt = try common.insertIdent(gpa, Ident.for_text("is_lt")),
            .is_lte = try common.insertIdent(gpa, Ident.for_text("is_lte")),
            .is_gt = try common.insertIdent(gpa, Ident.for_text("is_gt")),
            .is_gte = try common.insertIdent(gpa, Ident.for_text("is_gte")),
            .is_eq = try common.insertIdent(gpa, Ident.for_text("is_eq")),
            .@"try" = try common.insertIdent(gpa, Ident.for_text("Try")),
            .out_of_range = try common.insertIdent(gpa, Ident.for_text("OutOfRange")),
            .builtin_module = try common.insertIdent(gpa, Ident.for_text("Builtin")),
            .str = try common.insertIdent(gpa, Ident.for_text("Str")),
            .list = try common.insertIdent(gpa, Ident.for_text("List")),
            .box = try common.insertIdent(gpa, Ident.for_text("Box")),
            .builtin_try = try common.insertIdent(gpa, Ident.for_text("Try")),
            .builtin_numeral = try common.insertIdent(gpa, Ident.for_text("Num.Numeral")),
            .builtin_str = try common.insertIdent(gpa, Ident.for_text("Builtin.Str")),
            .u8_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.U8")),
            .i8_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.I8")),
            .u16_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.U16")),
            .i16_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.I16")),
            .u32_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.U32")),
            .i32_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.I32")),
            .u64_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.U64")),
            .i64_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.I64")),
            .u128_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.U128")),
            .i128_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.I128")),
            .f32_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.F32")),
            .f64_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.F64")),
            .dec_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.Dec")),
            .before_dot = try common.insertIdent(gpa, Ident.for_text("before_dot")),
            .after_dot = try common.insertIdent(gpa, Ident.for_text("after_dot")),
            .provided_by_compiler = try common.insertIdent(gpa, Ident.for_text("ProvidedByCompiler")),
            .tag = try common.insertIdent(gpa, Ident.for_text("tag")),
            .payload = try common.insertIdent(gpa, Ident.for_text("payload")),
            .is_negative = try common.insertIdent(gpa, Ident.for_text("is_negative")),
            .digits_before_pt = try common.insertIdent(gpa, Ident.for_text("digits_before_pt")),
            .digits_after_pt = try common.insertIdent(gpa, Ident.for_text("digits_after_pt")),
            .box_method = try common.insertIdent(gpa, Ident.for_text("box")),
            .unbox_method = try common.insertIdent(gpa, Ident.for_text("unbox")),
            .to_inspect = try common.insertIdent(gpa, Ident.for_text("to_inspect")),
            .ok = try common.insertIdent(gpa, Ident.for_text("Ok")),
            .err = try common.insertIdent(gpa, Ident.for_text("Err")),
            .from_numeral = try common.insertIdent(gpa, Ident.for_text("from_numeral")),
            .true_tag = try common.insertIdent(gpa, Ident.for_text("True")),
            .false_tag = try common.insertIdent(gpa, Ident.for_text("False")),
            // from_utf8 result fields
            .byte_index = try common.insertIdent(gpa, Ident.for_text("byte_index")),
            .string = try common.insertIdent(gpa, Ident.for_text("string")),
            .is_ok = try common.insertIdent(gpa, Ident.for_text("is_ok")),
            .problem_code = try common.insertIdent(gpa, Ident.for_text("problem_code")),
            // from_utf8 error payload fields (BadUtf8 record)
            .problem = try common.insertIdent(gpa, Ident.for_text("problem")),
            .index = try common.insertIdent(gpa, Ident.for_text("index")),
            // Synthetic identifiers for ? operator desugaring
            .question_ok = try common.insertIdent(gpa, Ident.for_text("#ok")),
            .question_err = try common.insertIdent(gpa, Ident.for_text("#err")),
        };
    }

    /// Find all well-known identifiers in a CommonEnv that has already interned them.
    /// Use this when loading a pre-compiled module where identifiers are already present.
    /// Panics if any identifier is not found (indicates corrupted/incompatible pre-compiled data).
    pub fn find(common: *const CommonEnv) CommonIdents {
        return .{
            .from_int_digits = common.findIdent(Ident.FROM_INT_DIGITS_METHOD_NAME) orelse unreachable,
            .from_dec_digits = common.findIdent(Ident.FROM_DEC_DIGITS_METHOD_NAME) orelse unreachable,
            .plus = common.findIdent(Ident.PLUS_METHOD_NAME) orelse unreachable,
            .minus = common.findIdent("minus") orelse unreachable,
            .times = common.findIdent("times") orelse unreachable,
            .div_by = common.findIdent("div_by") orelse unreachable,
            .div_trunc_by = common.findIdent("div_trunc_by") orelse unreachable,
            .rem_by = common.findIdent("rem_by") orelse unreachable,
            .negate = common.findIdent(Ident.NEGATE_METHOD_NAME) orelse unreachable,
            .abs = common.findIdent("abs") orelse unreachable,
            .abs_diff = common.findIdent("abs_diff") orelse unreachable,
            .not = common.findIdent("not") orelse unreachable,
            .is_lt = common.findIdent("is_lt") orelse unreachable,
            .is_lte = common.findIdent("is_lte") orelse unreachable,
            .is_gt = common.findIdent("is_gt") orelse unreachable,
            .is_gte = common.findIdent("is_gte") orelse unreachable,
            .is_eq = common.findIdent("is_eq") orelse unreachable,
            .@"try" = common.findIdent("Try") orelse unreachable,
            .out_of_range = common.findIdent("OutOfRange") orelse unreachable,
            .builtin_module = common.findIdent("Builtin") orelse unreachable,
            .str = common.findIdent("Str") orelse unreachable,
            .list = common.findIdent("List") orelse unreachable,
            .box = common.findIdent("Box") orelse unreachable,
            .builtin_try = common.findIdent("Try") orelse unreachable,
            .builtin_numeral = common.findIdent("Num.Numeral") orelse unreachable,
            .builtin_str = common.findIdent("Builtin.Str") orelse unreachable,
            .u8_type = common.findIdent("Builtin.Num.U8") orelse unreachable,
            .i8_type = common.findIdent("Builtin.Num.I8") orelse unreachable,
            .u16_type = common.findIdent("Builtin.Num.U16") orelse unreachable,
            .i16_type = common.findIdent("Builtin.Num.I16") orelse unreachable,
            .u32_type = common.findIdent("Builtin.Num.U32") orelse unreachable,
            .i32_type = common.findIdent("Builtin.Num.I32") orelse unreachable,
            .u64_type = common.findIdent("Builtin.Num.U64") orelse unreachable,
            .i64_type = common.findIdent("Builtin.Num.I64") orelse unreachable,
            .u128_type = common.findIdent("Builtin.Num.U128") orelse unreachable,
            .i128_type = common.findIdent("Builtin.Num.I128") orelse unreachable,
            .f32_type = common.findIdent("Builtin.Num.F32") orelse unreachable,
            .f64_type = common.findIdent("Builtin.Num.F64") orelse unreachable,
            .dec_type = common.findIdent("Builtin.Num.Dec") orelse unreachable,
            .before_dot = common.findIdent("before_dot") orelse unreachable,
            .after_dot = common.findIdent("after_dot") orelse unreachable,
            .provided_by_compiler = common.findIdent("ProvidedByCompiler") orelse unreachable,
            .tag = common.findIdent("tag") orelse unreachable,
            .payload = common.findIdent("payload") orelse unreachable,
            .is_negative = common.findIdent("is_negative") orelse unreachable,
            .digits_before_pt = common.findIdent("digits_before_pt") orelse unreachable,
            .digits_after_pt = common.findIdent("digits_after_pt") orelse unreachable,
            .box_method = common.findIdent("box") orelse unreachable,
            .unbox_method = common.findIdent("unbox") orelse unreachable,
            .to_inspect = common.findIdent("to_inspect") orelse unreachable,
            .ok = common.findIdent("Ok") orelse unreachable,
            .err = common.findIdent("Err") orelse unreachable,
            .from_numeral = common.findIdent("from_numeral") orelse unreachable,
            .true_tag = common.findIdent("True") orelse unreachable,
            .false_tag = common.findIdent("False") orelse unreachable,
            // from_utf8 result fields
            .byte_index = common.findIdent("byte_index") orelse unreachable,
            .string = common.findIdent("string") orelse unreachable,
            .is_ok = common.findIdent("is_ok") orelse unreachable,
            .problem_code = common.findIdent("problem_code") orelse unreachable,
            // from_utf8 error payload fields (BadUtf8 record)
            .problem = common.findIdent("problem") orelse unreachable,
            .index = common.findIdent("index") orelse unreachable,
            // Synthetic identifiers for ? operator desugaring
            .question_ok = common.findIdent("#ok") orelse unreachable,
            .question_err = common.findIdent("#err") orelse unreachable,
        };
    }
};

/// Key for method identifier lookup: (type_ident, method_ident) pair.
pub const MethodKey = packed struct(u64) {
    type_ident: Ident.Idx,
    method_ident: Ident.Idx,
};

/// Mapping from (type_ident, method_ident) pairs to their qualified method ident.
/// This enables O(log n) index-based method lookup instead of O(n) string comparison.
/// The value is the qualified method ident (e.g., "Bool.is_eq" for type "Bool" and method "is_eq").
///
/// This is populated during canonicalization when methods are defined in associated blocks.
pub const MethodIdents = SortedArrayBuilder(MethodKey, Ident.Idx);

gpa: std.mem.Allocator,

common: CommonEnv,
types: TypeStore,

// ===== Module compilation fields =====
// NOTE: These fields are populated during canonicalization and preserved for later use

/// The kind of module (type_module, app, etc.) - set during canonicalization
module_kind: ModuleKind,
/// All the definitions in the module (populated by canonicalization)
all_defs: CIR.Def.Span,
/// All the top-level statements in the module (populated by canonicalization)
all_statements: CIR.Statement.Span,
/// Definitions that are exported by this module (populated by canonicalization)
exports: CIR.Def.Span,
/// Required type signatures for platform modules (from `requires {} { main! : () => {} }`)
/// Maps identifier names to their expected type annotations.
/// Empty for non-platform modules.
requires_types: RequiredType.SafeList,
/// All builtin stmts (temporary until module imports are working)
builtin_statements: CIR.Statement.Span,
/// All external declarations referenced in this module
external_decls: CIR.ExternalDecl.SafeList,
/// Store for interned module imports
imports: CIR.Import.Store,
/// The module's name as a string
/// This is needed for import resolution to match import names to modules
module_name: []const u8,
/// The module's name as an interned identifier (for fast comparisons)
module_name_idx: Ident.Idx,
/// Diagnostics collected during canonicalization (optional)
diagnostics: CIR.Diagnostic.Span,
/// Stores the raw nodes which represent the intermediate representation
/// Uses an efficient data structure, and provides helpers for storing and retrieving nodes.
store: NodeStore,

/// Dependency analysis results (evaluation order for defs)
/// Set after canonicalization completes. Must not be accessed before then.
evaluation_order: ?*DependencyGraph.EvaluationOrder,

/// Well-known identifiers for type checking, operator desugaring, and layout generation.
/// Interned once during init to avoid repeated string comparisons.
idents: CommonIdents,

/// Deferred numeric literals collected during type checking
/// These will be validated during comptime evaluation
deferred_numeric_literals: DeferredNumericLiteral.SafeList,

/// Import mapping for type display names in error messages.
/// Maps fully-qualified type identifiers to their shortest display names based on imports.
/// Built during canonicalization when processing import statements.
/// Example: "MyModule.Foo" -> "F" if user has `import MyModule exposing [Foo as F]`
import_mapping: types_mod.import_mapping.ImportMapping,

/// Mapping from (type_ident, method_ident) pairs to qualified method idents.
/// Enables O(1) index-based method lookup during type checking and evaluation.
/// Populated during canonicalization when methods are defined in associated blocks.
method_idents: MethodIdents,

/// Deferred numeric literal for compile-time validation
pub const DeferredNumericLiteral = struct {
    expr_idx: CIR.Expr.Idx,
    type_var: TypeVar,
    constraint: types_mod.StaticDispatchConstraint,
    region: Region,

    pub const SafeList = collections.SafeList(@This());
};

/// Required type for platform modules - maps an identifier to its expected type annotation.
/// Used to enforce that apps provide values matching the platform's required types.
pub const RequiredType = struct {
    /// The identifier name (e.g., "main!")
    ident: Ident.Idx,
    /// The canonicalized type annotation for this required value
    type_anno: CIR.TypeAnno.Idx,
    /// Region of the requirement for error reporting
    region: Region,

    pub const SafeList = collections.SafeList(@This());
};

/// Relocate all pointers in the ModuleEnv by the given offset.
/// This is used when loading a ModuleEnv from shared memory at a different address.
pub fn relocate(self: *Self, offset: isize) void {
    // Relocate all sub-structures that contain pointers
    self.common.relocate(offset);
    self.types.relocate(offset);
    self.external_decls.relocate(offset);
    self.requires_types.relocate(offset);
    self.imports.relocate(offset);
    self.store.relocate(offset);
    self.deferred_numeric_literals.relocate(offset);
    self.method_idents.relocate(offset);

    // Relocate the module_name pointer if it's not empty
    if (self.module_name.len > 0) {
        const old_ptr = @intFromPtr(self.module_name.ptr);
        const new_ptr = @as(isize, @intCast(old_ptr)) + offset;
        self.module_name.ptr = @ptrFromInt(@as(usize, @intCast(new_ptr)));
    }
}

/// Initialize the compilation fields in an existing ModuleEnv
pub fn initCIRFields(self: *Self, module_name: []const u8) !void {
    self.module_kind = .deprecated_module; // default until canonicalization sets the actual kind
    self.all_defs = .{ .span = .{ .start = 0, .len = 0 } };
    self.all_statements = .{ .span = .{ .start = 0, .len = 0 } };
    self.exports = .{ .span = .{ .start = 0, .len = 0 } };
    self.builtin_statements = .{ .span = .{ .start = 0, .len = 0 } };
    // Note: external_decls already exists from ModuleEnv.init(), so we don't create a new one
    self.imports = CIR.Import.Store.init();
    self.module_name = module_name;
    self.module_name_idx = try self.insertIdent(Ident.for_text(module_name));
    self.diagnostics = CIR.Diagnostic.Span{ .span = base.DataSpan{ .start = 0, .len = 0 } };
    // Note: self.store already exists from ModuleEnv.init(), so we don't create a new one
    self.evaluation_order = null; // Will be set after canonicalization completes
}

/// Alias for initCIRFields for backwards compatibility with tests
pub fn initModuleEnvFields(self: *Self, module_name: []const u8) !void {
    return self.initCIRFields(module_name);
}

/// Initialize the module environment.
pub fn init(gpa: std.mem.Allocator, source: []const u8) std.mem.Allocator.Error!Self {
    // TODO: maybe wire in smarter default based on the initial input text size.

    var common = try CommonEnv.init(gpa, source);
    const idents = try CommonIdents.insert(gpa, &common);

    return Self{
        .gpa = gpa,
        .common = common,
        .types = try TypeStore.initCapacity(gpa, 2048, 512),
        .module_kind = .deprecated_module, // Set during canonicalization
        .all_defs = .{ .span = .{ .start = 0, .len = 0 } },
        .all_statements = .{ .span = .{ .start = 0, .len = 0 } },
        .exports = .{ .span = .{ .start = 0, .len = 0 } },
        .requires_types = try RequiredType.SafeList.initCapacity(gpa, 4),
        .builtin_statements = .{ .span = .{ .start = 0, .len = 0 } },
        .external_decls = try CIR.ExternalDecl.SafeList.initCapacity(gpa, 16),
        .imports = CIR.Import.Store.init(),
        .module_name = undefined, // Will be set later during canonicalization
        .module_name_idx = Ident.Idx.NONE, // Will be set later during canonicalization
        .diagnostics = CIR.Diagnostic.Span{ .span = base.DataSpan{ .start = 0, .len = 0 } },
        .store = try NodeStore.initCapacity(gpa, 10_000), // Default node store capacity
        .evaluation_order = null, // Will be set after canonicalization completes
        .idents = idents,
        .deferred_numeric_literals = try DeferredNumericLiteral.SafeList.initCapacity(gpa, 32),
        .import_mapping = types_mod.import_mapping.ImportMapping.init(gpa),
        .method_idents = MethodIdents.init(),
    };
}

/// Deinitialize the module environment.
pub fn deinit(self: *Self) void {
    self.common.deinit(self.gpa);
    self.types.deinit();
    self.external_decls.deinit(self.gpa);
    self.requires_types.deinit(self.gpa);
    self.imports.deinit(self.gpa);
    self.deferred_numeric_literals.deinit(self.gpa);
    self.import_mapping.deinit();
    self.method_idents.deinit(self.gpa);
    // diagnostics are stored in the NodeStore, no need to free separately
    self.store.deinit();

    if (self.evaluation_order) |eval_order| {
        eval_order.deinit();
        self.gpa.destroy(eval_order);
    }
}

// ===== Module compilation functionality =====

/// Records a diagnostic error during canonicalization without blocking compilation.
pub fn pushDiagnostic(self: *Self, reason: CIR.Diagnostic) std.mem.Allocator.Error!void {
    _ = try self.addDiagnostic(reason);
}

/// Creates a malformed node that represents a runtime error in the IR.
pub fn pushMalformed(self: *Self, comptime RetIdx: type, reason: CIR.Diagnostic) std.mem.Allocator.Error!RetIdx {
    comptime if (!isCastable(RetIdx)) @compileError("Idx type " ++ @typeName(RetIdx) ++ " is not castable");
    const diag_idx = try self.addDiagnostic(reason);
    const region = getDiagnosticRegion(reason);
    const malformed_idx = try self.addMalformed(diag_idx, region);
    return castIdx(Node.Idx, RetIdx, malformed_idx);
}

/// Extract the region from any diagnostic variant
fn getDiagnosticRegion(diagnostic: CIR.Diagnostic) Region {
    return switch (diagnostic) {
        .type_redeclared => |data| data.redeclared_region,
        .type_alias_redeclared => |data| data.redeclared_region,
        .nominal_type_redeclared => |data| data.redeclared_region,
        .duplicate_record_field => |data| data.duplicate_region,
        inline else => |data| data.region,
    };
}

/// Import helper functions from CIR
const isCastable = CIR.isCastable;
/// Cast function for safely converting between compatible index types
pub const castIdx = CIR.castIdx;

// ===== Module compilation functions =====

/// Retrieve all diagnostics collected during canonicalization.
pub fn getDiagnostics(self: *Self) std.mem.Allocator.Error![]CIR.Diagnostic {
    // Get all diagnostics from the store, not just the ones in self.diagnostics span
    const all_diagnostics = try self.store.diagnosticSpanFrom(0);
    const diagnostic_indices = self.store.sliceDiagnostics(all_diagnostics);
    const diagnostics = try self.gpa.alloc(CIR.Diagnostic, diagnostic_indices.len);
    for (diagnostic_indices, 0..) |diagnostic_idx, i| {
        diagnostics[i] = self.store.getDiagnostic(diagnostic_idx);
    }
    return diagnostics;
}

/// Compilation error report type for user-friendly error messages
pub const Report = CIR.Report;

/// Convert a canonicalization diagnostic to a Report for rendering.
pub fn diagnosticToReport(self: *Self, diagnostic: CIR.Diagnostic, allocator: std.mem.Allocator, filename: []const u8) !Report {
    return switch (diagnostic) {
        .invalid_num_literal => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            // Extract the literal text from the source
            const literal_text = self.getSource(data.region);

            var report = Report.init(allocator, "INVALID NUMBER", .runtime_error);
            const owned_literal = try report.addOwnedString(literal_text);

            try report.document.addReflowingText("This number literal is not valid: ");
            try report.document.addInlineCode(owned_literal);
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            try report.document.addLineBreak();
            try report.document.addReflowingText("Check that the number is correctly formatted. Valid examples include: ");
            try report.document.addInlineCode("42");
            try report.document.addReflowingText(", ");
            try report.document.addInlineCode("3.14");
            try report.document.addReflowingText(", ");
            try report.document.addInlineCode("0x1A");
            try report.document.addReflowingText(", or ");
            try report.document.addInlineCode("1_000_000");
            try report.document.addReflowingText(".");

            break :blk report;
        },
        .ident_not_in_scope => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);
            const ident_name = self.getIdent(data.ident);

            var report = Report.init(allocator, "UNDEFINED VARIABLE", .runtime_error);
            const owned_ident = try report.addOwnedString(ident_name);
            try report.document.addReflowingText("Nothing is named ");
            try report.document.addUnqualifiedSymbol(owned_ident);
            try report.document.addReflowingText(" in this scope.");
            try report.document.addLineBreak();
            try report.document.addReflowingText("Is there an ");
            try report.document.addKeyword("import");
            try report.document.addReflowingText(" or ");
            try report.document.addKeyword("exposing");
            try report.document.addReflowingText(" missing up-top?");
            try report.document.addLineBreak();
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .qualified_ident_does_not_exist => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);
            const ident_name = self.getIdent(data.ident);

            var report = Report.init(allocator, "DOES NOT EXIST", .runtime_error);
            const owned_ident = try report.addOwnedString(ident_name);
            try report.document.addUnqualifiedSymbol(owned_ident);
            try report.document.addReflowingText(" does not exist.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .exposed_but_not_implemented => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "EXPOSED BUT NOT DEFINED", .runtime_error);

            const ident_name = self.getIdent(data.ident);
            const owned_ident = try report.addOwnedString(ident_name);

            try report.document.addReflowingText("The module header says that ");
            try report.document.addUnqualifiedSymbol(owned_ident);
            try report.document.addReflowingText(" is exposed, but it is not defined anywhere in this module.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            // Add source context with location
            const owned_filename = try report.addOwnedString(filename);
            try report.addSourceContext(region_info, owned_filename, self.getSourceAll(), self.getLineStartsAll());

            try report.document.addReflowingText("You can fix this by either defining ");
            try report.document.addUnqualifiedSymbol(owned_ident);
            try report.document.addReflowingText(" in this module, or by removing it from the list of exposed values.");

            break :blk report;
        },
        .unused_variable => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);
            const ident_name = self.getIdent(data.ident);

            var report = Report.init(allocator, "UNUSED VARIABLE", .warning);
            const owned_ident = try report.addOwnedString(ident_name);

            try report.document.addReflowingText("Variable ");
            try report.document.addUnqualifiedSymbol(owned_ident);
            try report.document.addReflowingText(" is not used anywhere in your code.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            const MAX_IDENT_FIXED_BUFFER = 100;
            if (owned_ident.len > MAX_IDENT_FIXED_BUFFER - 1) {
                try report.document.addReflowingText("If you don't need this variable, prefix it with an underscore to suppress this warning.");
            } else {
                // format the identifier with an underscore
                try report.document.addReflowingText("If you don't need this variable, prefix it with an underscore like ");
                var buf: [MAX_IDENT_FIXED_BUFFER]u8 = undefined;
                const owned_ident_with_underscore = try std.fmt.bufPrint(&buf, "_{s}", .{owned_ident});

                try report.document.addUnqualifiedSymbol(owned_ident_with_underscore);
                try report.document.addReflowingText(" to suppress this warning.");
            }

            try report.document.addLineBreak();
            try report.document.addReflowingText("The unused variable is declared here:");
            try report.document.addLineBreak();

            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .underscore_in_type_declaration => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "UNDERSCORE IN TYPE ALIAS", .runtime_error);

            const kind = if (data.is_alias) "alias" else "opaque type";
            const message = try std.fmt.allocPrint(allocator, "Underscores are not allowed in type {s} declarations.", .{kind});
            defer allocator.free(message);
            const owned_message = try report.addOwnedString(message);
            try report.document.addReflowingText(owned_message);
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            // Add source context with location
            const owned_filename = try report.addOwnedString(filename);
            try report.addSourceContext(region_info, owned_filename, self.getSourceAll(), self.getLineStartsAll());

            try report.document.addLineBreak();
            const explanation = try std.fmt.allocPrint(allocator, "Underscores in type annotations mean \"I don't care about this type\", which doesn't make sense when declaring a type. If you need a placeholder type variable, use a named type variable like `a` instead.", .{});
            defer allocator.free(explanation);
            const owned_explanation = try report.addOwnedString(explanation);
            try report.document.addReflowingText(owned_explanation);

            break :blk report;
        },
        .undeclared_type => |data| blk: {
            const type_name = self.getIdent(data.name);
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "UNDECLARED TYPE", .runtime_error);
            const owned_type_name = try report.addOwnedString(type_name);
            try report.document.addReflowingText("The type ");
            try report.document.addType(owned_type_name);
            try report.document.addReflowingText(" is not declared in this scope.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("This type is referenced here:");
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .type_alias_but_needed_nominal => |data| blk: {
            const type_name = self.getIdent(data.name);
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "EXPECTED NOMINAL TYPE", .runtime_error);
            const owned_type_name = try report.addOwnedString(type_name);
            try report.document.addReflowingText("You are using the type ");
            try report.document.addType(owned_type_name);
            try report.document.addReflowingText(" like a nominal type, but it is an alias.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("This type is referenced here:");
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            try report.document.addLineBreak();
            try report.document.addLineBreak();
            try report.document.addAnnotated("Hint:", .emphasized);
            try report.document.addReflowingText(" You can declare this type with ");
            try report.document.addInlineCode(":=");
            try report.document.addReflowingText(" to make it nominal.");

            break :blk report;
        },
        .type_redeclared => |data| blk: {
            const type_name = self.getIdent(data.name);
            const original_region_info = self.calcRegionInfo(data.original_region);
            const redeclared_region_info = self.calcRegionInfo(data.redeclared_region);

            var report = Report.init(allocator, "TYPE REDECLARED", .runtime_error);
            const owned_type_name = try report.addOwnedString(type_name);
            try report.document.addReflowingText("The type ");
            try report.document.addType(owned_type_name);
            try report.document.addReflowingText(" is being redeclared.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            // Show where the redeclaration is
            try report.document.addReflowingText("The redeclaration is here:");
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                redeclared_region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            try report.document.addLineBreak();
            try report.document.addReflowingText("But ");
            try report.document.addType(owned_type_name);
            try report.document.addReflowingText(" was already declared here:");
            try report.document.addLineBreak();
            try report.document.addSourceRegion(
                original_region_info,
                .dimmed,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .invalid_top_level_statement => |data| blk: {
            const stmt_name = self.getString(data.stmt);
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "INVALID STATEMENT", .runtime_error);
            const owned_stmt = try report.addOwnedString(stmt_name);
            try report.document.addReflowingText("The statement ");
            try report.document.addInlineCode(owned_stmt);
            try report.document.addReflowingText(" is not allowed at the top level.");
            try report.document.addLineBreak();
            try report.document.addReflowingText("Only definitions, type annotations, and imports are allowed at the top level.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .used_underscore_variable => |data| blk: {
            const ident_name = self.getIdent(data.ident);
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "UNDERSCORE VARIABLE USED", .warning);
            const owned_ident = try report.addOwnedString(ident_name);

            try report.document.addReflowingText("Variable ");
            try report.document.addUnqualifiedSymbol(owned_ident);
            try report.document.addReflowingText(" is prefixed with an underscore but is actually used.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("Variables prefixed with ");
            try report.document.addUnqualifiedSymbol("_");
            try report.document.addReflowingText(" are intended to be unused. Remove the underscore prefix: ");

            // Create the suggested name without underscore
            const suggested_name = ident_name[1..]; // Remove first character (_)
            const owned_suggested = try report.addOwnedString(suggested_name);
            try report.document.addUnqualifiedSymbol(owned_suggested);
            try report.document.addReflowingText(".");
            try report.document.addLineBreak();
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .warning_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .expr_not_canonicalized => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "UNRECOGNIZED SYNTAX", .runtime_error);
            try report.document.addReflowingText("I don't recognize this syntax.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            try report.document.addLineBreak();
            try report.document.addReflowingText("This might be a syntax error, an unsupported language feature, or a typo.");

            break :blk report;
        },
        .crash_expects_string => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "CRASH EXPECTS STRING", .runtime_error);
            try report.document.addReflowingText("The ");
            try report.document.addAnnotated("crash", .inline_code);
            try report.document.addReflowingText(" keyword expects a string literal as its argument.");
            try report.document.addLineBreak();
            try report.document.addReflowingText("For example: ");
            try report.document.addAnnotated("crash \"Something went wrong\"", .inline_code);
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .duplicate_record_field => |data| blk: {
            const field_name = self.getIdent(data.field_name);
            const duplicate_region_info = self.calcRegionInfo(data.duplicate_region);
            const original_region_info = self.calcRegionInfo(data.original_region);

            var report = Report.init(allocator, "DUPLICATE RECORD FIELD", .runtime_error);
            const owned_field_name = try report.addOwnedString(field_name);

            try report.document.addReflowingText("The record field ");
            try report.document.addRecordField(owned_field_name);
            try report.document.addReflowingText(" appears more than once in this record.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            // Show where the duplicate field is
            try report.document.addReflowingText("This field is duplicated here:");
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                duplicate_region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            try report.document.addLineBreak();
            try report.document.addReflowingText("The field ");
            try report.document.addRecordField(owned_field_name);
            try report.document.addReflowingText(" was first defined here:");
            try report.document.addLineBreak();
            try report.document.addSourceRegion(
                original_region_info,
                .dimmed,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            try report.document.addLineBreak();
            try report.document.addReflowingText("Record fields must have unique names. Consider renaming one of these fields or removing the duplicate.");

            break :blk report;
        },
        .redundant_exposed => |data| blk: {
            const ident_name = self.getIdent(data.ident);
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "REDUNDANT EXPOSED", .warning);
            const owned_ident = try report.addOwnedString(ident_name);

            try report.document.addReflowingText("The identifier ");
            try report.document.addUnqualifiedSymbol(owned_ident);
            try report.document.addReflowingText(" is exposed multiple times in the module header.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            try report.document.addReflowingText("You can remove the duplicate entry to fix this warning.");

            break :blk report;
        },
        .undeclared_type_var => |data| blk: {
            const type_var_name = self.getIdent(data.name);
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "UNDECLARED TYPE VARIABLE", .runtime_error);
            const owned_type_var_name = try report.addOwnedString(type_var_name);
            try report.document.addReflowingText("The type variable ");
            try report.document.addType(owned_type_var_name);
            try report.document.addReflowingText(" is not declared in this scope.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("Type variables must be introduced in a type annotation before they can be used.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("This type variable is referenced here:");
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .not_implemented => |data| blk: {
            const feature = self.getString(data.feature);
            var report = Report.init(allocator, "NOT IMPLEMENTED", .fatal);
            const owned_feature = try report.addOwnedString(feature);
            try report.document.addReflowingText("This feature is not yet implemented: ");
            try report.document.addAnnotatedText(owned_feature, .emphasized);
            try report.document.addLineBreak();
            try report.document.addLineBreak();
            try report.document.addReflowingText("This error doesn't have a proper diagnostic report yet. Let us know if you want to help improve Roc's error messages!");
            break :blk report;
        },
        .malformed_type_annotation => |data| blk: {
            var report = Report.init(allocator, "MALFORMED TYPE", .runtime_error);
            try report.document.addReflowingText("This type annotation is malformed or contains invalid syntax.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            const owned_filename = try report.addOwnedString(filename);
            const region_info = self.calcRegionInfo(data.region);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .if_condition_not_canonicalized => |_| blk: {
            var report = Report.init(allocator, "INVALID IF CONDITION", .runtime_error);
            try report.document.addReflowingText("The condition in this ");
            try report.document.addKeyword("if");
            try report.document.addReflowingText(" expression could not be processed.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();
            try report.document.addReflowingText("The condition must be a valid expression that evaluates to a ");
            try report.document.addKeyword("Bool");
            try report.document.addReflowingText(" value (");
            try report.document.addKeyword("Bool.true");
            try report.document.addReflowingText(" or ");
            try report.document.addKeyword("Bool.false");
            try report.document.addReflowingText(").");
            break :blk report;
        },
        .if_then_not_canonicalized => |_| blk: {
            var report = Report.init(allocator, "INVALID IF BRANCH", .runtime_error);
            try report.document.addReflowingText("The branch in this ");
            try report.document.addKeyword("if");
            try report.document.addReflowingText(" expression could not be processed.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();
            try report.document.addReflowingText("The branch must contain a valid expression. Check for syntax errors or missing values.");
            break :blk report;
        },
        .if_else_not_canonicalized => |_| blk: {
            var report = Report.init(allocator, "INVALID IF BRANCH", .runtime_error);
            try report.document.addReflowingText("The ");
            try report.document.addKeyword("else");
            try report.document.addReflowingText(" branch of this ");
            try report.document.addKeyword("if");
            try report.document.addReflowingText(" expression could not be processed.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();
            try report.document.addReflowingText("The ");
            try report.document.addKeyword("else");
            try report.document.addReflowingText(" branch must contain a valid expression. Check for syntax errors or missing values.");
            try report.document.addLineBreak();
            break :blk report;
        },
        .if_expr_without_else => |_| blk: {
            var report = Report.init(allocator, "IF EXPRESSION WITHOUT ELSE", .runtime_error);
            try report.document.addReflowingText("This ");
            try report.document.addKeyword("if");
            try report.document.addReflowingText(" has no ");
            try report.document.addKeyword("else");
            try report.document.addReflowingText(" branch, but it's being used as an expression (assigned to a variable, passed to a function, etc.).");
            try report.document.addLineBreak();
            try report.document.addLineBreak();
            try report.document.addReflowingText("You can only use ");
            try report.document.addKeyword("if");
            try report.document.addReflowingText(" without ");
            try report.document.addKeyword("else");
            try report.document.addReflowingText(" when it's a statement. When ");
            try report.document.addKeyword("if");
            try report.document.addReflowingText(" is used as an expression that evaluates to a value, ");
            try report.document.addKeyword("else");
            try report.document.addReflowingText(" is required because otherwise there wouldn't always be a value available.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();
            try report.document.addReflowingText("Either add an ");
            try report.document.addKeyword("else");
            try report.document.addReflowingText(" branch, or use this ");
            try report.document.addKeyword("if");
            try report.document.addReflowingText(" as a standalone statement.");
            break :blk report;
        },
        .pattern_not_canonicalized => |_| blk: {
            var report = Report.init(allocator, "INVALID PATTERN", .runtime_error);
            try report.document.addReflowingText("This pattern contains invalid syntax or uses unsupported features.");
            break :blk report;
        },
        .pattern_arg_invalid => |_| blk: {
            var report = Report.init(allocator, "INVALID PATTERN ARGUMENT", .runtime_error);
            try report.document.addReflowingText("Pattern arguments must be valid patterns like identifiers, literals, or destructuring patterns.");
            break :blk report;
        },
        .shadowing_warning => |data| blk: {
            const ident_name = self.getIdent(data.ident);
            const new_region_info = self.calcRegionInfo(data.region);
            const original_region_info = self.calcRegionInfo(data.original_region);

            var report = Report.init(allocator, "DUPLICATE DEFINITION", .warning);
            const owned_ident = try report.addOwnedString(ident_name);
            try report.document.addReflowingText("The name ");
            try report.document.addUnqualifiedSymbol(owned_ident);
            try report.document.addReflowingText(" is being redeclared in this scope.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            // Show where the new declaration is
            try report.document.addReflowingText("The redeclaration is here:");
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                new_region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            try report.document.addLineBreak();
            try report.document.addReflowingText("But ");
            try report.document.addUnqualifiedSymbol(owned_ident);
            try report.document.addReflowingText(" was already defined here:");
            try report.document.addLineBreak();
            try report.document.addSourceRegion(
                original_region_info,
                .dimmed,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .empty_tuple => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "EMPTY TUPLE NOT ALLOWED", .runtime_error);
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addReflowingText("I am part way through parsing this tuple, but it is empty:");
            try report.document.addLineBreak();
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );
            try report.document.addLineBreak();
            try report.document.addReflowingText("If you want to represent nothing, try using an empty record: ");
            try report.document.addAnnotated("{}", .inline_code);
            try report.document.addReflowingText(".");

            break :blk report;
        },
        .lambda_body_not_canonicalized => blk: {
            var report = Report.init(allocator, "INVALID LAMBDA", .runtime_error);
            try report.document.addReflowingText("The body of this lambda expression is not valid.");

            break :blk report;
        },
        .malformed_where_clause => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "MALFORMED WHERE CLAUSE", .runtime_error);
            try report.document.addReflowingText("This where clause could not be parsed correctly.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );
            try report.document.addLineBreak();
            try report.document.addReflowingText("Check the syntax of your where clause.");

            break :blk report;
        },
        .var_across_function_boundary => blk: {
            var report = Report.init(allocator, "VAR REASSIGNMENT ERROR", .runtime_error);
            try report.document.addReflowingText("Cannot reassign a ");
            try report.document.addKeyword("var");
            try report.document.addReflowingText(" from outside the function where it was declared.");
            try report.document.addLineBreak();
            try report.document.addReflowingText("Variables declared with ");
            try report.document.addKeyword("var");
            try report.document.addReflowingText(" can only be reassigned within the same function scope.");

            break :blk report;
        },
        .tuple_elem_not_canonicalized => blk: {
            var report = Report.init(allocator, "INVALID TUPLE ELEMENT", .runtime_error);
            try report.document.addReflowingText("This tuple element is malformed or contains invalid syntax.");

            break :blk report;
        },
        .f64_pattern_literal => |data| blk: {
            // Extract the literal text from the source
            const literal_text = self.getSourceAll()[data.region.start.offset..data.region.end.offset];

            var report = Report.init(allocator, "F64 NOT ALLOWED IN PATTERN", .runtime_error);

            // Format the message to match origin/main
            try report.document.addText("This floating-point literal cannot be used in a pattern match: ");
            try report.document.addInlineCode(literal_text);
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("This number exceeds the precision range of Roc's ");
            try report.document.addInlineCode("Dec");
            try report.document.addReflowingText(" type and would require F64 representation. ");
            try report.document.addReflowingText("Floating-point numbers (F64) cannot be used in patterns because they don't have reliable equality comparison.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addText("Consider one of these alternatives:");
            try report.document.addLineBreak();
            try report.document.addText("• Use a guard condition with a range check");
            try report.document.addLineBreak();
            try report.document.addText("• Use a smaller number that fits in Dec's precision");
            try report.document.addLineBreak();
            try report.document.addText("• Restructure your code to avoid pattern matching on this value");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addText("For example, instead of:");
            try report.document.addLineBreak();
            try report.document.addInlineCode("1e100 => ...");
            try report.document.addLineBreak();
            try report.document.addText("Use a guard:");
            try report.document.addLineBreak();
            try report.document.addInlineCode("n if n > 1e99 => ...");

            break :blk report;
        },
        .type_not_exposed => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "TYPE NOT EXPOSED", .runtime_error);

            const type_name_bytes = self.getIdent(data.type_name);
            const type_name = try report.addOwnedString(type_name_bytes);

            const module_name_bytes = self.getIdent(data.module_name);
            const module_name = try report.addOwnedString(module_name_bytes);

            // Format the message to match origin/main
            try report.document.addText("The type ");
            try report.document.addInlineCode(type_name);
            try report.document.addReflowingText(" is not exposed by the module ");
            try report.document.addInlineCode(module_name);
            try report.document.addReflowingText(".");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("You're attempting to use this type here:");
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .value_not_exposed => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "VALUE NOT EXPOSED", .runtime_error);

            // Format the message to match origin/main
            try report.document.addText("The value ");
            try report.document.addInlineCode(self.getIdent(data.value_name));
            try report.document.addReflowingText(" is not exposed by the module ");
            try report.document.addInlineCode(self.getIdent(data.module_name));
            try report.document.addReflowingText(".");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("You're attempting to use this value here:");
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .module_not_found => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "MODULE NOT FOUND", .runtime_error);

            const module_name_bytes = self.getIdent(data.module_name);
            const module_name = try report.addOwnedString(module_name_bytes);

            // Format the message to match origin/main
            try report.document.addText("The module ");
            try report.document.addInlineCode(module_name);
            try report.document.addReflowingText(" was not found in this Roc project.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("You're attempting to use this module here:");
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .module_not_imported => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "MODULE NOT IMPORTED", .runtime_error);

            const module_name_bytes = self.getIdent(data.module_name);
            const module_name = try report.addOwnedString(module_name_bytes);

            // Format the message to match origin/main
            try report.document.addText("There is no module with the name ");
            try report.document.addInlineCode(module_name);
            try report.document.addReflowingText(" imported into this Roc file.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("You're attempting to use this module here:");
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .nested_type_not_found => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "MISSING NESTED TYPE", .runtime_error);

            const parent_bytes = self.getIdent(data.parent_name);
            const parent_name = try report.addOwnedString(parent_bytes);

            const nested_bytes = self.getIdent(data.nested_name);
            const nested_name = try report.addOwnedString(nested_bytes);

            try report.document.addInlineCode(parent_name);
            try report.document.addReflowingText(" is in scope, but it doesn't have a nested type ");

            if (std.mem.eql(u8, parent_bytes, nested_bytes)) {
                // Say "also named" if the parent and nested types are equal, e.g. `Foo.Foo` - when
                // this happens it can be kind of a confusing message if the message just says
                // "Foo is in scope, but it doesn't have a nested type named Foo" compared to
                // "Foo is in scope, but it doesn't have a nested type that's also named Foo"
                try report.document.addReflowingText("that's also ");
            }

            try report.document.addReflowingText("named ");
            try report.document.addInlineCode(nested_name);
            try report.document.addReflowingText(".");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("It's referenced here:");
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .nested_value_not_found => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "DOES NOT EXIST", .runtime_error);

            const parent_bytes = self.getIdent(data.parent_name);
            const parent_name = try report.addOwnedString(parent_bytes);

            const nested_bytes = self.getIdent(data.nested_name);
            const nested_name = try report.addOwnedString(nested_bytes);

            // First line: "Foo.bar does not exist."
            const full_name = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ parent_bytes, nested_bytes });
            defer allocator.free(full_name);
            const owned_full_name = try report.addOwnedString(full_name);
            try report.document.addInlineCode(owned_full_name);
            try report.document.addReflowingText(" does not exist.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            // Second line: "Foo is in scope, but it has no associated bar."
            try report.document.addInlineCode(parent_name);
            try report.document.addReflowingText(" is in scope, but it has no associated ");
            try report.document.addInlineCode(nested_name);
            try report.document.addReflowingText(".");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("It's referenced here:");
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .where_clause_not_allowed_in_type_decl => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "WHERE CLAUSE NOT ALLOWED IN TYPE DECLARATION", .warning);

            // Format the message to match origin/main
            try report.document.addText("You cannot define a ");
            try report.document.addInlineCode("where");
            try report.document.addReflowingText(" clause inside a type declaration.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("You're attempting do this here:");
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .type_module_missing_matching_type => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "TYPE MODULE MISSING MATCHING TYPE", .runtime_error);

            const module_name_bytes = self.getIdent(data.module_name);
            const module_name = try report.addOwnedString(module_name_bytes);

            try report.document.addReflowingText("Type modules must have a type declaration matching the module name.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addText("This file is named ");
            try report.document.addInlineCode(module_name);
            try report.document.addReflowingText(".roc, but no top-level type declaration named ");
            try report.document.addInlineCode(module_name);
            try report.document.addReflowingText(" was found.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("Add either:");
            try report.document.addLineBreak();
            const nominal_msg = try std.fmt.allocPrint(allocator, "{s} := ...", .{module_name_bytes});
            defer allocator.free(nominal_msg);
            const owned_nominal = try report.addOwnedString(nominal_msg);
            try report.document.addInlineCode(owned_nominal);
            try report.document.addReflowingText(" (nominal type)");
            try report.document.addLineBreak();
            try report.document.addReflowingText("or:");
            try report.document.addLineBreak();
            const alias_msg = try std.fmt.allocPrint(allocator, "{s} : ...", .{module_name_bytes});
            defer allocator.free(alias_msg);
            const owned_alias = try report.addOwnedString(alias_msg);
            try report.document.addInlineCode(owned_alias);
            try report.document.addReflowingText(" (type alias)");
            try report.document.addLineBreak();

            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .default_app_missing_main => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "MISSING MAIN! FUNCTION", .runtime_error);

            try report.document.addReflowingText("Default app modules must have a ");
            try report.document.addInlineCode("main!");
            try report.document.addReflowingText(" function.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addText("No ");
            try report.document.addInlineCode("main!");
            try report.document.addReflowingText(" function was found.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("Add a main! function like:");
            try report.document.addLineBreak();
            try report.document.addInlineCode("main! = |arg| { ... }");
            try report.document.addLineBreak();

            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .default_app_wrong_arity => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "MAIN! SHOULD TAKE 1 ARGUMENT", .runtime_error);

            try report.document.addInlineCode("main!");
            try report.document.addReflowingText(" is defined but has the wrong number of arguments. ");
            try report.document.addInlineCode("main!");
            try report.document.addReflowingText(" should take 1 argument.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            const arity_msg = try std.fmt.allocPrint(allocator, "{d}", .{data.arity});
            defer allocator.free(arity_msg);
            const owned_arity = try report.addOwnedString(arity_msg);
            try report.document.addText("Found ");
            try report.document.addInlineCode(owned_arity);
            try report.document.addReflowingText(" arguments.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("Change it to:");
            try report.document.addLineBreak();
            try report.document.addInlineCode("main! = |arg| { ... }");
            try report.document.addLineBreak();

            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .cannot_import_default_app => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "CANNOT IMPORT DEFAULT APP", .runtime_error);

            const module_name_bytes = self.getIdent(data.module_name);
            const module_name = try report.addOwnedString(module_name_bytes);

            try report.document.addReflowingText("You cannot import a default app module.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addText("The module ");
            try report.document.addInlineCode(module_name);
            try report.document.addReflowingText(" is a default app module and cannot be imported.");
            try report.document.addLineBreak();

            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .execution_requires_app_or_default_app => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "EXECUTION REQUIRES APP OR DEFAULT APP", .runtime_error);

            try report.document.addReflowingText("This file cannot be executed because it is not an app or default-app module.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("Add either:");
            try report.document.addLineBreak();
            try report.document.addInlineCode("app");
            try report.document.addReflowingText(" header at the top of the file");
            try report.document.addLineBreak();
            try report.document.addReflowingText("or:");
            try report.document.addLineBreak();
            try report.document.addReflowingText("a ");
            try report.document.addInlineCode("main!");
            try report.document.addReflowingText(" function with 1 argument (for default-app)");
            try report.document.addLineBreak();

            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .type_name_case_mismatch => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "TYPE NAME CASE MISMATCH", .runtime_error);

            const module_name_bytes = self.getIdent(data.module_name);
            const module_name = try report.addOwnedString(module_name_bytes);
            const type_name_bytes = self.getIdent(data.type_name);
            const type_name = try report.addOwnedString(type_name_bytes);

            try report.document.addReflowingText("Type module name must match the type declaration.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addText("This file is named ");
            try report.document.addInlineCode(module_name);
            try report.document.addReflowingText(".roc, but the type is named ");
            try report.document.addInlineCode(type_name);
            try report.document.addReflowingText(".");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("Make sure the type name matches the filename exactly (case-sensitive).");
            try report.document.addLineBreak();

            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .module_header_deprecated => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "MODULE HEADER DEPRECATED", .warning);

            try report.document.addReflowingText("The ");
            try report.document.addInlineCode("module");
            try report.document.addReflowingText(" header is deprecated.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("Type modules (headerless files with a top-level type matching the filename) are now the preferred way to define modules.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("Remove the ");
            try report.document.addInlineCode("module");
            try report.document.addReflowingText(" header and ensure your file defines a type that matches the filename.");
            try report.document.addLineBreak();

            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .warning_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .redundant_expose_main_type => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "REDUNDANT EXPOSE", .warning);

            const type_name_bytes = self.getIdent(data.type_name);
            const type_name = try report.addOwnedString(type_name_bytes);
            const module_name_bytes = self.getIdent(data.module_name);
            const module_name = try report.addOwnedString(module_name_bytes);

            try report.document.addReflowingText("Redundantly exposing ");
            try report.document.addInlineCode(type_name);
            try report.document.addReflowingText(" when importing ");
            try report.document.addInlineCode(module_name);
            try report.document.addReflowingText(".");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("The type ");
            try report.document.addInlineCode(type_name);
            try report.document.addReflowingText(" is automatically exposed when importing a type module.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("Remove ");
            try report.document.addInlineCode(type_name);
            try report.document.addReflowingText(" from the exposing clause.");
            try report.document.addLineBreak();

            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .warning_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .invalid_main_type_rename_in_exposing => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);

            var report = Report.init(allocator, "INVALID TYPE RENAME", .runtime_error);

            const type_name_bytes = self.getIdent(data.type_name);
            const type_name = try report.addOwnedString(type_name_bytes);
            const alias_bytes = self.getIdent(data.alias);
            const alias = try report.addOwnedString(alias_bytes);

            try report.document.addReflowingText("Cannot rename ");
            try report.document.addInlineCode(type_name);
            try report.document.addReflowingText(" to ");
            try report.document.addInlineCode(alias);
            try report.document.addReflowingText(" in the exposing clause.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();

            try report.document.addReflowingText("To rename both the module and its main type, use ");
            try report.document.addInlineCode("as");
            try report.document.addReflowingText(" at the module level:");
            try report.document.addLineBreak();

            const example_msg = try std.fmt.allocPrint(allocator, "import ModuleName as {s}", .{alias_bytes});
            defer allocator.free(example_msg);
            const owned_example = try report.addOwnedString(example_msg);
            try report.document.addInlineCode(owned_example);
            try report.document.addLineBreak();

            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        .ident_already_in_scope => |data| blk: {
            const region_info = self.calcRegionInfo(data.region);
            const ident_name = self.getIdent(data.ident);

            var report = Report.init(allocator, "SHADOWING", .runtime_error);
            const owned_ident = try report.addOwnedString(ident_name);
            try report.document.addReflowingText("The name ");
            try report.document.addUnqualifiedSymbol(owned_ident);
            try report.document.addReflowingText(" is already defined in this scope.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();
            try report.document.addReflowingText("Choose a different name for this identifier.");
            try report.document.addLineBreak();
            try report.document.addLineBreak();
            const owned_filename = try report.addOwnedString(filename);
            try report.document.addSourceRegion(
                region_info,
                .error_highlight,
                owned_filename,
                self.getSourceAll(),
                self.getLineStartsAll(),
            );

            break :blk report;
        },
        else => unreachable, // All diagnostics must have explicit handlers
    };
}

/// Get region info for a given region
pub fn getRegionInfo(self: *const Self, region: Region) !RegionInfo {
    return self.common.getRegionInfo(region);
}

/// Returns diagnostic position information for the given region.
/// This is a standalone utility function that takes the source text as a parameter
/// to avoid storing it in the cacheable IR structure.
pub fn calcRegionInfo(self: *const Self, region: Region) RegionInfo {
    return self.common.calcRegionInfo(region);
}

/// Extract a literal from source code between given byte offsets
pub fn literal_from_source(self: *const Self, start_offset: u32, end_offset: u32) []const u8 {
    return self.common.source[start_offset..end_offset];
}

/// Get the source line for a given region
pub fn getSourceLine(self: *const Self, region: Region) ![]const u8 {
    return self.common.getSourceLine(region);
}

/// Serialized representation of ModuleEnv.
/// Uses extern struct to guarantee consistent field layout across optimization levels.
pub const Serialized = extern struct {
    gpa: [2]u64, // Reserve space for allocator (vtable ptr + context ptr), provided during deserialization
    common: CommonEnv.Serialized,
    types: TypeStore.Serialized,
    all_defs: CIR.Def.Span,
    all_statements: CIR.Statement.Span,
    exports: CIR.Def.Span,
    requires_types: RequiredType.SafeList.Serialized,
    builtin_statements: CIR.Statement.Span,
    external_decls: CIR.ExternalDecl.SafeList.Serialized,
    imports: CIR.Import.Store.Serialized,
    module_name: [2]u64, // Reserve space for slice (ptr + len), provided during deserialization
    module_name_idx_reserved: u32, // Reserved space for module_name_idx field (interned during deserialization)
    diagnostics: CIR.Diagnostic.Span,
    store: NodeStore.Serialized,
    module_kind: ModuleKind.Serialized,
    evaluation_order_reserved: u64, // Reserved space for evaluation_order field (required for in-place deserialization cast)
    // Well-known identifier indices (serialized directly, no lookup needed during deserialization)
    idents: CommonIdents,
    deferred_numeric_literals: DeferredNumericLiteral.SafeList.Serialized,
    import_mapping_reserved: [6]u64, // Reserved space for import_mapping (AutoHashMap is ~40 bytes), initialized at runtime
    method_idents: MethodIdents.Serialized,

    /// Serialize a ModuleEnv into this Serialized struct, appending data to the writer
    pub fn serialize(
        self: *Serialized,
        env: *const Self,
        allocator: std.mem.Allocator,
        writer: *CompactWriter,
    ) !void {
        try self.common.serialize(&env.common, allocator, writer);
        try self.types.serialize(&env.types, allocator, writer);

        // Copy simple values directly
        self.module_kind = ModuleKind.Serialized.encode(env.module_kind);
        self.all_defs = env.all_defs;
        self.all_statements = env.all_statements;
        self.exports = env.exports;
        self.builtin_statements = env.builtin_statements;

        try self.requires_types.serialize(&env.requires_types, allocator, writer);
        try self.external_decls.serialize(&env.external_decls, allocator, writer);
        try self.imports.serialize(&env.imports, allocator, writer);

        self.diagnostics = env.diagnostics;

        // Serialize NodeStore
        try self.store.serialize(&env.store, allocator, writer);

        // Serialize deferred numeric literals (will be empty during serialization since it's only used during type checking/evaluation)
        try self.deferred_numeric_literals.serialize(&env.deferred_numeric_literals, allocator, writer);

        // Set gpa, module_name, module_name_idx_reserved, evaluation_order_reserved to zeros;
        // these are runtime-only and will be set during deserialization.
        self.gpa = .{ 0, 0 };
        self.module_name = .{ 0, 0 };
        self.module_name_idx_reserved = 0;
        self.evaluation_order_reserved = 0;

        // Serialize well-known identifier indices directly (no lookup needed during deserialization)
        self.idents = env.idents;
        // import_mapping is runtime-only and initialized fresh during deserialization
        self.import_mapping_reserved = .{ 0, 0, 0, 0, 0, 0 };
        // Serialize method_idents map
        try self.method_idents.serialize(&env.method_idents, allocator, writer);
    }

    /// Deserialize a ModuleEnv from the buffer, updating the ModuleEnv in place
    pub fn deserialize(
        self: *Serialized,
        offset: i64,
        gpa: std.mem.Allocator,
        source: []const u8,
        module_name: []const u8,
    ) std.mem.Allocator.Error!*Self {
        // Verify that Serialized is at least as large as the runtime struct.
        // This is required because we're reusing the same memory location.
        // On 32-bit platforms, Serialized may be larger due to using fixed-size types for platform-independent serialization.
        // In Debug builds, Self may be larger due to debug-only store tracking fields, so skip this check.
        comptime {
            if (builtin.mode != .Debug) {
                std.debug.assert(@sizeOf(@This()) >= @sizeOf(Self));
            }
        }

        // Overwrite ourself with the deserialized version, and return our pointer after casting it to Self.
        const env = @as(*Self, @ptrFromInt(@intFromPtr(self)));

        // Deserialize common env first so we can look up identifiers
        const common = self.common.deserialize(offset, source).*;

        env.* = Self{
            .gpa = gpa,
            .common = common,
            .types = self.types.deserialize(offset, gpa).*,
            .module_kind = self.module_kind.decode(),
            .all_defs = self.all_defs,
            .all_statements = self.all_statements,
            .exports = self.exports,
            .requires_types = self.requires_types.deserialize(offset).*,
            .builtin_statements = self.builtin_statements,
            .external_decls = self.external_decls.deserialize(offset).*,
            .imports = (try self.imports.deserialize(offset, gpa)).*,
            .module_name = module_name,
            .module_name_idx = Ident.Idx.NONE, // Not used for deserialized modules (only needed during fresh canonicalization)
            .diagnostics = self.diagnostics,
            .store = self.store.deserialize(offset, gpa).*,
            .evaluation_order = null, // Not serialized, will be recomputed if needed
            .idents = self.idents,
            .deferred_numeric_literals = self.deferred_numeric_literals.deserialize(offset).*,
            .import_mapping = types_mod.import_mapping.ImportMapping.init(gpa),
            .method_idents = self.method_idents.deserialize(offset).*,
        };

        return env;
    }
};

/// Convert a type into a node index
pub fn nodeIdxFrom(idx: anytype) Node.Idx {
    return @enumFromInt(@intFromEnum(idx));
}

/// Convert a type into a type var
pub fn varFrom(idx: anytype) TypeVar {
    return @enumFromInt(@intFromEnum(idx));
}

/// Adds an identifier to the list of exposed items by its identifier index.
pub fn addExposedById(self: *Self, ident_idx: Ident.Idx) !void {
    return try self.common.exposed_items.addExposedById(self.gpa, @bitCast(ident_idx));
}

/// Associates a node index with an exposed identifier.
pub fn setExposedNodeIndexById(self: *Self, ident_idx: Ident.Idx, node_idx: u16) !void {
    return try self.common.exposed_items.setNodeIndexById(self.gpa, @bitCast(ident_idx), node_idx);
}

/// Retrieves the node index associated with an exposed identifier, if any.
pub fn getExposedNodeIndexById(self: *const Self, ident_idx: Ident.Idx) ?u16 {
    return self.common.getNodeIndexById(self.gpa, ident_idx);
}

/// Get the exposed node index for a type given its statement index.
/// This is used for auto-imported builtin types where we have the statement index pre-computed.
/// For auto-imported types, the statement index IS the node/var index directly.
pub fn getExposedNodeIndexByStatementIdx(self: *const Self, stmt_idx: CIR.Statement.Idx) ?u16 {
    _ = self; // Not needed for this simplified implementation

    // For auto-imported builtin types (Bool, Try, etc.), the statement index
    // IS the node/var index. This is because type declarations get type variables
    // indexed by their statement index, not by their position in arrays.
    const node_idx: u16 = @intCast(@intFromEnum(stmt_idx));
    return node_idx;
}

/// Ensures that the exposed items are sorted by identifier index.
pub fn ensureExposedSorted(self: *Self, allocator: std.mem.Allocator) void {
    self.common.exposed_items.ensureSorted(allocator);
}

/// Checks whether the given identifier is exposed by this module.
pub fn containsExposedById(self: *const Self, ident_idx: Ident.Idx) bool {
    return self.common.exposed_items.containsById(self.gpa, @bitCast(ident_idx));
}

/// Assert that nodes and regions are in sync
pub inline fn debugAssertArraysInSync(self: *const Self) void {
    if (builtin.mode == .Debug) {
        const cir_nodes = self.store.nodes.items.len;
        const region_nodes = self.store.regions.len();

        if (!(cir_nodes == region_nodes)) {
            std.debug.panic(
                "Arrays out of sync:\n  cir_nodes={}\n  region_nodes={}\n",
                .{ cir_nodes, region_nodes },
            );
        }
    }
}

/// Assert that nodes, regions and types are all in sync
inline fn debugAssertIdxsEql(comptime desc: []const u8, idx1: anytype, idx2: anytype) void {
    if (builtin.mode == .Debug) {
        const idx1_int = @intFromEnum(idx1);
        const idx2_int = @intFromEnum(idx2);

        if (idx1_int != idx2_int) {
            std.debug.panic(
                "{s} idxs out of sync: {} != {}\n",
                .{ desc, idx1_int, idx2_int },
            );
        }
    }
}

/// Add a new expression to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addDef(self: *Self, expr: CIR.Def, region: Region) std.mem.Allocator.Error!CIR.Def.Idx {
    const expr_idx = try self.store.addDef(expr, region);
    self.debugAssertArraysInSync();
    return expr_idx;
}

/// Add a new type header to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addTypeHeader(self: *Self, expr: CIR.TypeHeader, region: Region) std.mem.Allocator.Error!CIR.TypeHeader.Idx {
    const expr_idx = try self.store.addTypeHeader(expr, region);
    self.debugAssertArraysInSync();
    return expr_idx;
}

/// Add a new statement to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addStatement(self: *Self, expr: CIR.Statement, region: Region) std.mem.Allocator.Error!CIR.Statement.Idx {
    const expr_idx = try self.store.addStatement(expr, region);
    self.debugAssertArraysInSync();
    return expr_idx;
}

/// Add a new pattern to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addPattern(self: *Self, expr: CIR.Pattern, region: Region) std.mem.Allocator.Error!CIR.Pattern.Idx {
    const expr_idx = try self.store.addPattern(expr, region);
    self.debugAssertArraysInSync();
    return expr_idx;
}

/// Add a new expression to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addExpr(self: *Self, expr: CIR.Expr, region: Region) std.mem.Allocator.Error!CIR.Expr.Idx {
    const expr_idx = try self.store.addExpr(expr, region);
    self.debugAssertArraysInSync();
    return expr_idx;
}

/// Add a new capture to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addCapture(self: *Self, capture: CIR.Expr.Capture, region: Region) std.mem.Allocator.Error!CIR.Expr.Capture.Idx {
    const capture_idx = try self.store.addCapture(capture, region);
    self.debugAssertArraysInSync();
    return capture_idx;
}

/// Add a new record field to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addRecordField(self: *Self, expr: CIR.RecordField, region: Region) std.mem.Allocator.Error!CIR.RecordField.Idx {
    const expr_idx = try self.store.addRecordField(expr, region);
    self.debugAssertArraysInSync();
    return expr_idx;
}

/// Add a new record destructuring to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addRecordDestruct(self: *Self, expr: CIR.Pattern.RecordDestruct, region: Region) std.mem.Allocator.Error!CIR.Pattern.RecordDestruct.Idx {
    const expr_idx = try self.store.addRecordDestruct(expr, region);
    self.debugAssertArraysInSync();
    return expr_idx;
}

/// Adds a new if branch to the store.
/// This function asserts that the nodes and regions are in sync.
pub fn addIfBranch(self: *Self, expr: CIR.Expr.IfBranch, region: Region) std.mem.Allocator.Error!CIR.Expr.IfBranch.Idx {
    const expr_idx = try self.store.addIfBranch(expr, region);
    self.debugAssertArraysInSync();
    return expr_idx;
}

/// Add a new match branch to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addMatchBranch(self: *Self, expr: CIR.Expr.Match.Branch, region: Region) std.mem.Allocator.Error!CIR.Expr.Match.Branch.Idx {
    const expr_idx = try self.store.addMatchBranch(expr, region);
    self.debugAssertArraysInSync();
    return expr_idx;
}

/// Add a new where clause to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addWhereClause(self: *Self, expr: CIR.WhereClause, region: Region) std.mem.Allocator.Error!CIR.WhereClause.Idx {
    const expr_idx = try self.store.addWhereClause(expr, region);
    self.debugAssertArraysInSync();
    return expr_idx;
}

/// Add a new type annotation to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addTypeAnno(self: *Self, expr: CIR.TypeAnno, region: Region) std.mem.Allocator.Error!CIR.TypeAnno.Idx {
    const expr_idx = try self.store.addTypeAnno(expr, region);
    self.debugAssertArraysInSync();
    return expr_idx;
}

/// Add a new annotation to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addAnnotation(self: *Self, expr: CIR.Annotation, region: Region) std.mem.Allocator.Error!CIR.Annotation.Idx {
    const expr_idx = try self.store.addAnnotation(expr, region);
    self.debugAssertArraysInSync();
    return expr_idx;
}

/// Add a new record field to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addAnnoRecordField(self: *Self, expr: CIR.TypeAnno.RecordField, region: Region) std.mem.Allocator.Error!CIR.TypeAnno.RecordField.Idx {
    const expr_idx = try self.store.addAnnoRecordField(expr, region);
    self.debugAssertArraysInSync();
    return expr_idx;
}

/// Add a new exposed item to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addExposedItem(self: *Self, expr: CIR.ExposedItem, region: Region) std.mem.Allocator.Error!CIR.ExposedItem.Idx {
    const expr_idx = try self.store.addExposedItem(expr, region);
    self.debugAssertArraysInSync();
    return expr_idx;
}

/// Add a diagnostic.
/// This function asserts that the nodes and regions are in sync.
pub fn addDiagnostic(self: *Self, reason: CIR.Diagnostic) std.mem.Allocator.Error!CIR.Diagnostic.Idx {
    const expr_idx = try self.store.addDiagnostic(reason);
    self.debugAssertArraysInSync();
    return expr_idx;
}

/// Add a new malformed node to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addMalformed(self: *Self, diagnostic_idx: CIR.Diagnostic.Idx, region: Region) std.mem.Allocator.Error!CIR.Node.Idx {
    const malformed_idx = try self.store.addMalformed(diagnostic_idx, region);
    self.debugAssertArraysInSync();
    return malformed_idx;
}

/// Add a new match branch pattern to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addMatchBranchPattern(self: *Self, expr: CIR.Expr.Match.BranchPattern, region: Region) std.mem.Allocator.Error!CIR.Expr.Match.BranchPattern.Idx {
    const expr_idx = try self.store.addMatchBranchPattern(expr, region);
    self.debugAssertArraysInSync();
    return expr_idx;
}

/// Add a new pattern record field to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addPatternRecordField(self: *Self, expr: CIR.PatternRecordField) std.mem.Allocator.Error!CIR.PatternRecordField.Idx {
    const expr_idx = try self.store.addPatternRecordField(expr);
    self.debugAssertArraysInSync();
    return expr_idx;
}

/// Add a new type variable to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addTypeSlot(
    self: *Self,
    parent_node: CIR.Node.Idx,
    region: Region,
    comptime RetIdx: type,
) std.mem.Allocator.Error!RetIdx {
    comptime if (!isCastable(RetIdx)) @compileError("Idx type " ++ @typeName(RetIdx) ++ " is not castable");
    const node_idx = try self.store.addTypeVarSlot(parent_node, region);
    self.debugAssertArraysInSync();
    return @enumFromInt(@intFromEnum(node_idx));
}

/// Adds an external declaration and returns its index
pub fn pushExternalDecl(self: *Self, decl: CIR.ExternalDecl) std.mem.Allocator.Error!CIR.ExternalDecl.Idx {
    const idx = @as(u32, @intCast(self.external_decls.len()));
    _ = try self.external_decls.append(self.gpa, decl);
    return @enumFromInt(idx);
}

/// Retrieves an external declaration by its index
pub fn getExternalDecl(self: *const Self, idx: CIR.ExternalDecl.Idx) *const CIR.ExternalDecl {
    return self.external_decls.get(@as(CIR.ExternalDecl.SafeList.Idx, @enumFromInt(@intFromEnum(idx))));
}

/// Adds multiple external declarations and returns a span
pub fn pushExternalDecls(self: *Self, decls: []const CIR.ExternalDecl) std.mem.Allocator.Error!CIR.ExternalDecl.Span {
    const start = @as(u32, @intCast(self.external_decls.len()));
    for (decls) |decl| {
        _ = try self.external_decls.append(self.gpa, decl);
    }
    return CIR.ExternalDecl.Span{ .span = .{ .start = start, .len = @as(u32, @intCast(decls.len)) } };
}

/// Gets a slice of external declarations from a span
pub fn sliceExternalDecls(self: *const Self, span: CIR.ExternalDecl.Span) []const CIR.ExternalDecl {
    const range = CIR.ExternalDecl.SafeList.Range{ .start = @enumFromInt(span.span.start), .count = span.span.len };
    return self.external_decls.sliceRange(range);
}

/// Retrieves the text of an identifier by its index
pub fn getIdentText(self: *const Self, idx: Ident.Idx) []const u8 {
    return self.getIdent(idx);
}

/// Helper to format pattern index for s-expr output
fn formatPatternIdxNode(gpa: std.mem.Allocator, pattern_idx: CIR.Pattern.Idx) SExpr {
    var node = SExpr.init(gpa, "pid");
    node.appendUnsignedInt(gpa, @intFromEnum(pattern_idx));
    return node;
}

/// Helper function to generate the S-expression node for the entire module.
/// If a single expression is provided, only that expression is returned.
pub fn pushToSExprTree(self: *Self, maybe_expr_idx: ?CIR.Expr.Idx, tree: *SExprTree) std.mem.Allocator.Error!void {
    if (maybe_expr_idx) |expr_idx| {
        // Only output the given expression
        try self.store.getExpr(expr_idx).pushToSExprTree(self, tree, expr_idx);
    } else {
        const root_begin = tree.beginNode();
        try tree.pushStaticAtom("can-ir");

        // Iterate over all the definitions in the file and convert each to an S-expression tree
        const defs_slice = self.store.sliceDefs(self.all_defs);
        const statements_slice = self.store.sliceStatements(self.all_statements);

        if (defs_slice.len == 0 and statements_slice.len == 0 and self.external_decls.len() == 0) {
            try tree.pushBoolPair("empty", true);
        }
        const attrs = tree.beginNode();

        for (defs_slice) |def_idx| {
            try self.store.getDef(def_idx).pushToSExprTree(self, tree);
        }

        for (statements_slice) |stmt_idx| {
            try self.store.getStatement(stmt_idx).pushToSExprTree(self, tree, stmt_idx);
        }

        for (0..@intCast(self.external_decls.len())) |i| {
            const external_decl = self.external_decls.get(@enumFromInt(i));
            try external_decl.pushToSExprTree(self, tree);
        }

        try tree.endNode(root_begin, attrs);
    }
}

/// Append region information to an S-expression node for a given index.
pub fn appendRegionInfoToSExprTree(self: *const Self, tree: *SExprTree, idx: anytype) std.mem.Allocator.Error!void {
    const region = self.store.getNodeRegion(@enumFromInt(@intFromEnum(idx)));
    try self.appendRegionInfoToSExprTreeFromRegion(tree, region);
}

/// Append region information to an S-expression node from a specific region.
pub fn appendRegionInfoToSExprTreeFromRegion(self: *const Self, tree: *SExprTree, region: Region) std.mem.Allocator.Error!void {
    const info = self.getRegionInfo(region) catch RegionInfo{
        .start_line_idx = 0,
        .start_col_idx = 0,
        .end_line_idx = 0,
        .end_col_idx = 0,
    };
    try tree.pushBytesRange(
        region.start.offset,
        region.end.offset,
        info,
    );
}

/// Get region information for a node.
pub fn getNodeRegionInfo(self: *const Self, idx: anytype) RegionInfo {
    const region = self.store.getNodeRegion(@enumFromInt(@intFromEnum(idx)));
    return self.getRegionInfo(region);
}

/// Helper function to convert type information to an SExpr node
/// in S-expression format for snapshot testing. Implements the definition-focused
/// format showing final types for defs, expressions, and builtins.
pub fn pushTypesToSExprTree(self: *Self, maybe_expr_idx: ?CIR.Expr.Idx, tree: *SExprTree) std.mem.Allocator.Error!void {
    if (maybe_expr_idx) |expr_idx| {
        try self.pushExprTypesToSExprTree(expr_idx, tree);
    } else {
        // Generate full type information for all definitions and expressions
        const root_begin = tree.beginNode();
        try tree.pushStaticAtom("inferred-types");

        const root_attrs = tree.beginNode();

        // Create defs section
        const defs_begin = tree.beginNode();
        try tree.pushStaticAtom("defs");
        const defs_attrs = tree.beginNode();

        // Iterate through all definitions to extract pattern types
        const defs_slice = self.store.sliceDefs(self.all_defs);
        for (defs_slice) |def_idx| {
            const def = self.store.getDef(def_idx);

            // Only process assign patterns - skip destructuring patterns
            const pattern = self.store.getPattern(def.pattern);
            switch (pattern) {
                .assign => {},
                else => continue, // Skip non-assign patterns (like destructuring)
            }

            const pattern_var = varFrom(def.pattern);

            // Get the region for this definition
            const pattern_node_idx: CIR.Node.Idx = @enumFromInt(@intFromEnum(def.pattern));
            const pattern_region = self.store.getRegionAt(pattern_node_idx);

            // Create a TypeWriter to format the type
            var type_writer = self.initTypeWriter() catch continue;
            defer type_writer.deinit();

            // Write the type to the buffer
            type_writer.write(pattern_var) catch continue;

            // Add the pattern type entry
            const patt_begin = tree.beginNode();
            try tree.pushStaticAtom("patt");
            try self.appendRegionInfoToSExprTreeFromRegion(tree, pattern_region);

            const type_str = type_writer.get();
            try tree.pushStringPair("type", type_str);

            try tree.endNode(patt_begin, tree.beginNode());
        }

        try tree.endNode(defs_begin, defs_attrs);

        // Check if we have any type declarations to output
        const all_stmts = self.store.sliceStatements(self.all_statements);
        var has_type_decl = false;
        for (all_stmts) |stmt_idx| {
            const stmt = self.store.getStatement(stmt_idx);
            switch (stmt) {
                .s_alias_decl, .s_nominal_decl => {
                    has_type_decl = true;
                    break;
                },
                else => continue,
            }
        }

        // Create type_decls section if we have any type declarations
        if (has_type_decl) {
            const type_decls_begin = tree.beginNode();
            try tree.pushStaticAtom("type_decls");
            const type_decls_attrs = tree.beginNode();

            for (all_stmts) |stmt_idx| {
                const stmt = self.store.getStatement(stmt_idx);
                switch (stmt) {
                    .s_alias_decl => |alias| {
                        const stmt_begin = tree.beginNode();
                        try tree.pushStaticAtom("alias");

                        // Add region info for the statement
                        const stmt_region = self.store.getStatementRegion(stmt_idx);
                        try self.appendRegionInfoToSExprTreeFromRegion(tree, stmt_region);

                        // Get the type variable for this statement
                        const stmt_var = varFrom(stmt_idx);

                        // Create a TypeWriter to format the type
                        var type_writer = self.initTypeWriter() catch continue;
                        defer type_writer.deinit();

                        // Write the type to the buffer
                        type_writer.write(stmt_var) catch continue;

                        const type_str = type_writer.get();
                        try tree.pushStringPair("type", type_str);

                        const stmt_attrs = tree.beginNode();

                        // Add the type header
                        const header = self.store.getTypeHeader(alias.header);
                        try header.pushToSExprTree(self, tree, alias.header);

                        try tree.endNode(stmt_begin, stmt_attrs);
                    },
                    .s_nominal_decl => |nominal| {
                        const stmt_begin = tree.beginNode();
                        try tree.pushStaticAtom("nominal");

                        // Add region info for the statement
                        const stmt_region = self.store.getStatementRegion(stmt_idx);
                        try self.appendRegionInfoToSExprTreeFromRegion(tree, stmt_region);

                        // Get the type variable for this statement
                        const stmt_var = varFrom(stmt_idx);

                        // Create a TypeWriter to format the type
                        var type_writer = self.initTypeWriter() catch continue;
                        defer type_writer.deinit();

                        // Write the type to the buffer
                        type_writer.write(stmt_var) catch continue;

                        const type_str = type_writer.get();
                        try tree.pushStringPair("type", type_str);

                        const stmt_attrs = tree.beginNode();

                        // Add the type header
                        const header = self.store.getTypeHeader(nominal.header);
                        try header.pushToSExprTree(self, tree, nominal.header);

                        try tree.endNode(stmt_begin, stmt_attrs);
                    },
                    else => continue,
                }
            }

            try tree.endNode(type_decls_begin, type_decls_attrs);
        }

        // Create expressions section
        const exprs_begin = tree.beginNode();
        try tree.pushStaticAtom("expressions");
        const exprs_attrs = tree.beginNode();

        // Iterate through all definitions to extract expression types
        for (defs_slice) |def_idx| {
            const def = self.store.getDef(def_idx);
            const expr_var = varFrom(def.expr);

            // Get the region for this expression
            const expr_node_idx: CIR.Node.Idx = @enumFromInt(@intFromEnum(def.expr));
            const expr_region = self.store.getRegionAt(expr_node_idx);

            // Create a TypeWriter to format the type
            var type_writer = self.initTypeWriter() catch continue;
            defer type_writer.deinit();

            // Write the type to the buffer
            type_writer.write(expr_var) catch continue;

            // Add the expression type entry
            const expr_begin = tree.beginNode();
            try tree.pushStaticAtom("expr");
            try self.appendRegionInfoToSExprTreeFromRegion(tree, expr_region);

            const type_str = type_writer.get();
            try tree.pushStringPair("type", type_str);

            try tree.endNode(expr_begin, tree.beginNode());
        }

        try tree.endNode(exprs_begin, exprs_attrs);
        try tree.endNode(root_begin, root_attrs);
    }
}

fn pushExprTypesToSExprTree(self: *Self, expr_idx: CIR.Expr.Idx, tree: *SExprTree) std.mem.Allocator.Error!void {
    const expr_begin = tree.beginNode();
    try tree.pushStaticAtom("expr");

    // Add region info for the expression
    try self.appendRegionInfoToSExprTree(tree, expr_idx);

    // Get the type variable for this expression
    const expr_var = varFrom(expr_idx);

    // Create a TypeWriter to format the type
    var type_writer = try self.initTypeWriter();
    defer type_writer.deinit();

    // Write the type to the buffer
    try type_writer.write(expr_var);

    // Add the formatted type to the S-expression tree
    const type_str = type_writer.get();
    try tree.pushStringPair("type", type_str);

    try tree.endNode(expr_begin, tree.beginNode());
}

/// Retrieves a string literal by its index from the common environment.
pub fn getString(self: *const Self, idx: StringLiteral.Idx) []const u8 {
    return self.common.getString(idx);
}

/// Inserts a string literal into the common environment and returns its index.
pub fn insertString(self: *Self, string: []const u8) std.mem.Allocator.Error!StringLiteral.Idx {
    return try self.common.insertString(self.gpa, string);
}

/// Returns a mutable reference to the identifier store.
pub fn getIdentStore(self: *Self) *Ident.Store {
    return &self.common.idents;
}

/// Returns an immutable reference to the identifier store.
pub fn getIdentStoreConst(self: *const Self) *const Ident.Store {
    return &self.common.idents;
}

/// Retrieves the text of an identifier by its index.
pub fn getIdent(self: *const Self, idx: Ident.Idx) []const u8 {
    return self.common.getIdent(idx);
}

/// Get the source text for a given region
pub fn getSource(self: *const Self, region: Region) []const u8 {
    return self.common.getSource(region);
}

/// TODO this is a code smell... we should track down the places using this
/// and replace with something more sensible -- need to refactor diagnostics a little.
pub fn getSourceAll(self: *const Self) []const u8 {
    return self.common.getSourceAll();
}

/// TODO this is a code smell... we should track down the places using this
/// and replace with something more sensible -- need to refactor diagnostics a little.
pub fn getLineStartsAll(self: *const Self) []const u32 {
    return self.common.getLineStartsAll();
}

pub fn initTypeWriter(self: *Self) std.mem.Allocator.Error!TypeWriter {
    return TypeWriter.initFromParts(self.gpa, &self.types, self.getIdentStore(), null);
}

/// Inserts an identifier into the common environment and returns its index.
pub fn insertIdent(self: *Self, ident: Ident) std.mem.Allocator.Error!Ident.Idx {
    return try self.common.insertIdent(self.gpa, ident);
}

/// Creates and inserts a qualified identifier (e.g., "Foo.bar") into the common environment.
/// This handles the full lifecycle: building the qualified name, creating the Ident,
/// inserting it into the store, and cleaning up any temporary allocations.
/// All memory management is handled internally with no caller obligations.
pub fn insertQualifiedIdent(
    self: *Self,
    parent: []const u8,
    child: []const u8,
) std.mem.Allocator.Error!Ident.Idx {
    const total_len = parent.len + 1 + child.len; // parent + '.' + child

    if (total_len <= 256) {
        // Use stack buffer for small identifiers
        var buf: [256]u8 = undefined;
        const qualified = std.fmt.bufPrint(&buf, "{s}.{s}", .{ parent, child }) catch unreachable;
        return try self.insertIdent(Ident.for_text(qualified));
    } else {
        // Use heap allocation for large identifiers
        const qualified = try std.fmt.allocPrint(self.gpa, "{s}.{s}", .{ parent, child });
        defer self.gpa.free(qualified);
        return try self.insertIdent(Ident.for_text(qualified));
    }
}

/// Looks up a method identifier on a type by building the qualified method name.
/// This handles cross-module method lookup by building names like "Builtin.Num.U64.from_numeral".
///
/// Parameters:
/// - type_name: The type's identifier text (e.g., "Num.U64" or "Bool")
/// - method_name: The unqualified method name (e.g., "from_numeral")
///
/// Returns the method's ident index if found, or null if the method doesn't exist.
/// This is a read-only operation that doesn't modify the ident store.
pub fn getMethodIdent(self: *const Self, type_name: []const u8, method_name: []const u8) ?Ident.Idx {
    // Build the qualified method name: "{type_name}.{method_name}"
    // The type_name may already include the module prefix (e.g., "Num.U64")
    // or just be the type name (e.g., "Bool" for Builtin.Bool)
    const total_len = self.module_name.len + 1 + type_name.len + 1 + method_name.len;

    if (total_len <= 256) {
        // Use stack buffer for small identifiers
        var buf: [256]u8 = undefined;

        // Check if type_name already starts with module_name
        if (type_name.len > self.module_name.len and
            std.mem.startsWith(u8, type_name, self.module_name) and
            type_name[self.module_name.len] == '.')
        {
            // Type name is already qualified (e.g., "Builtin.Bool")
            const qualified = std.fmt.bufPrint(&buf, "{s}.{s}", .{ type_name, method_name }) catch return null;
            return self.getIdentStoreConst().findByString(qualified);
        } else if (std.mem.eql(u8, type_name, self.module_name)) {
            // Type name IS the module name (e.g., looking up method on "Builtin" itself)
            const qualified = std.fmt.bufPrint(&buf, "{s}.{s}", .{ type_name, method_name }) catch return null;
            return self.getIdentStoreConst().findByString(qualified);
        } else {
            // Try module-qualified name first (e.g., "Builtin.Num.U64.from_numeral")
            const qualified = std.fmt.bufPrint(&buf, "{s}.{s}.{s}", .{ self.module_name, type_name, method_name }) catch return null;
            if (self.getIdentStoreConst().findByString(qualified)) |idx| {
                return idx;
            }
            // Fallback: try without module prefix (e.g., "Color.as_str" for app-defined types)
            // This handles the case where methods are registered with just the type-qualified name
            const simple_qualified = std.fmt.bufPrint(&buf, "{s}.{s}", .{ type_name, method_name }) catch return null;
            return self.getIdentStoreConst().findByString(simple_qualified);
        }
    } else {
        // Use heap allocation for large identifiers (rare case)
        // Try module-qualified name first
        const qualified = if (type_name.len > self.module_name.len and
            std.mem.startsWith(u8, type_name, self.module_name) and
            type_name[self.module_name.len] == '.')
            std.fmt.allocPrint(self.gpa, "{s}.{s}", .{ type_name, method_name }) catch return null
        else if (std.mem.eql(u8, type_name, self.module_name))
            std.fmt.allocPrint(self.gpa, "{s}.{s}", .{ type_name, method_name }) catch return null
        else
            std.fmt.allocPrint(self.gpa, "{s}.{s}.{s}", .{ self.module_name, type_name, method_name }) catch return null;
        defer self.gpa.free(qualified);
        if (self.getIdentStoreConst().findByString(qualified)) |idx| {
            return idx;
        }
        // Fallback for the module-qualified case
        if (type_name.len <= self.module_name.len or
            !std.mem.startsWith(u8, type_name, self.module_name) or
            type_name[self.module_name.len] != '.')
        {
            const simple_qualified = std.fmt.allocPrint(self.gpa, "{s}.{s}", .{ type_name, method_name }) catch return null;
            defer self.gpa.free(simple_qualified);
            return self.getIdentStoreConst().findByString(simple_qualified);
        }
        return null;
    }
}

/// Registers a method identifier mapping for fast index-based lookup.
/// This should be called during canonicalization when a method is defined in an associated block.
///
/// Parameters:
/// - type_ident: The type's identifier index (e.g., the ident for "Bool")
/// - method_ident: The method's identifier index (e.g., the ident for "is_eq")
/// - qualified_ident: The qualified method ident (e.g., "Bool.is_eq")
pub fn registerMethodIdent(self: *Self, type_ident: Ident.Idx, method_ident: Ident.Idx, qualified_ident: Ident.Idx) !void {
    const key = MethodKey{ .type_ident = type_ident, .method_ident = method_ident };
    try self.method_idents.put(self.gpa, key, qualified_ident);
}

/// Looks up a method identifier by type and method ident indices.
/// This is the fast O(log n) index-based lookup that avoids string comparison.
///
/// Parameters:
/// - type_ident: The type's identifier index (must be in this module's ident store)
/// - method_ident: The method's identifier index (must be in this module's ident store)
///
/// Returns the qualified method's ident index if found, or null if not registered.
pub fn lookupMethodIdent(self: *Self, type_ident: Ident.Idx, method_ident: Ident.Idx) ?Ident.Idx {
    const key = MethodKey{ .type_ident = type_ident, .method_ident = method_ident };
    return self.method_idents.get(self.gpa, key);
}

/// Looks up a method identifier by type and method ident indices (const version).
/// This is the fast O(log n) index-based lookup that avoids string comparison.
pub fn lookupMethodIdentConst(self: *const Self, type_ident: Ident.Idx, method_ident: Ident.Idx) ?Ident.Idx {
    const key = MethodKey{ .type_ident = type_ident, .method_ident = method_ident };
    // Cast away const for the get operation (it doesn't modify the structure, just ensures sorted)
    const mutable_self = @constCast(self);
    return mutable_self.method_idents.get(self.gpa, key);
}

/// Looks up a method identifier by translating idents from a source environment.
/// This first finds the corresponding idents in this module, then does index-based lookup.
///
/// Parameters:
/// - source_env: The module environment where type_ident and method_ident are from
/// - type_ident: The type's identifier index in source_env
/// - method_ident: The method's identifier index in source_env
///
/// Returns the qualified method's ident index if found, or null if the method doesn't exist.
/// Falls back to string-based getMethodIdent for backward compatibility with pre-compiled modules.
pub fn lookupMethodIdentFromEnv(self: *Self, source_env: *const Self, type_ident: Ident.Idx, method_ident: Ident.Idx) ?Ident.Idx {
    // First, try to find the type and method idents in our own ident store
    const type_name = source_env.getIdent(type_ident);
    const method_name = source_env.getIdent(method_ident);

    // Find corresponding idents in this module
    const local_type_ident = self.common.findIdent(type_name) orelse return null;
    const local_method_ident = self.common.findIdent(method_name) orelse return null;

    // Try index-based lookup first (O(log n))
    if (self.lookupMethodIdent(local_type_ident, local_method_ident)) |result| {
        return result;
    }

    // Fall back to string-based lookup for backward compatibility with pre-compiled modules
    // that don't have method_idents populated. This can be removed once all modules are recompiled.
    return self.getMethodIdent(type_name, method_name);
}

/// Const version of lookupMethodIdentFromEnv for use with immutable module environments.
/// Safe to use on deserialized modules since method_idents is already sorted.
/// Falls back to string-based getMethodIdent for backward compatibility with pre-compiled modules.
pub fn lookupMethodIdentFromEnvConst(self: *const Self, source_env: *const Self, type_ident: Ident.Idx, method_ident: Ident.Idx) ?Ident.Idx {
    // First, try to find the type and method idents in our own ident store
    const type_name = source_env.getIdent(type_ident);
    const method_name = source_env.getIdent(method_ident);

    // Find corresponding idents in this module
    const local_type_ident = self.common.findIdent(type_name) orelse return null;
    const local_method_ident = self.common.findIdent(method_name) orelse return null;

    // Try index-based lookup first (O(log n))
    if (self.lookupMethodIdentConst(local_type_ident, local_method_ident)) |result| {
        return result;
    }

    // Fall back to string-based lookup for backward compatibility with pre-compiled modules
    // that don't have method_idents populated. This can be removed once all modules are recompiled.
    return self.getMethodIdent(type_name, method_name);
}

/// Looks up a method identifier when the type and method idents come from different source environments.
/// This is needed when e.g. type_ident is from runtime layout store and method_ident is from CIR.
/// Falls back to string-based getMethodIdent for backward compatibility with pre-compiled modules.
pub fn lookupMethodIdentFromTwoEnvsConst(
    self: *const Self,
    type_source_env: *const Self,
    type_ident: Ident.Idx,
    method_source_env: *const Self,
    method_ident: Ident.Idx,
) ?Ident.Idx {
    // Get strings from respective source environments
    const type_name = type_source_env.getIdent(type_ident);
    const method_name = method_source_env.getIdent(method_ident);

    // Find corresponding idents in this module
    const local_type_ident = self.common.findIdent(type_name) orelse return null;
    const local_method_ident = self.common.findIdent(method_name) orelse return null;

    // Try index-based lookup first (O(log n))
    if (self.lookupMethodIdentConst(local_type_ident, local_method_ident)) |result| {
        return result;
    }

    // Fall back to string-based lookup for backward compatibility with pre-compiled modules
    // that don't have method_idents populated. This can be removed once all modules are recompiled.
    return self.getMethodIdent(type_name, method_name);
}

/// Returns the line start positions for source code position mapping.
/// Each element represents the byte offset where a new line begins.
pub fn getLineStarts(self: *const Self) []const u32 {
    return self.common.getLineStartsAll();
}
